/**
 * This file is part of decent-cpp.
 *
 * decent-cpp is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * decent-cpp is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with decent-cpp.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "ws.h"
#include <boost/bind.hpp>
#include <nlohmann/json.hpp>
#include <nonstd/optional.hpp>
#include <tuple>
#include "crypto.h"
#include "data.h"
#include "http.h"

using nlohmann::json;
using nonstd::make_optional;
using nonstd::nullopt;
using nonstd::optional;
using std::shared_ptr;
using std::tuple;
typedef std::shared_ptr<SimpleWeb::asio::io_service> HttpIoService;

namespace dc {
#ifdef USE_HTTPS
WsServer ws_server(HTTPS_CERT, HTTPS_KEY);
#else
WsServer ws_server;
#endif
volatile bool running = false;
std::unordered_multimap<uuid, shared_ptr<WsServer::Connection>>
    authedConnections;
// Absolutely hacks and super dangerous
std::unordered_map<WsServer::Connection*, uuid> reverseAuthedConnections;

void removeConn(std::shared_ptr<WsServer::Connection> const& connection) {
  WsServer::Connection* rawConn = connection.get();
  auto it = reverseAuthedConnections.find(rawConn);
  if (it != reverseAuthedConnections.end()) {
    uuid u = it->second;
    reverseAuthedConnections.erase(it);
    auto it2 = authedConnections.equal_range(u);
    auto it3 = it2.first;
    for (; it3 != it2.second; ++it3) {
      if (it3->second.get() == rawConn) {
        authedConnections.erase(it3);
        break;
      }
    }

    // Emit user/offline
    auto sessObj = Session::getSession(u);
    if (sessObj.has_value()) {
      User user = sessObj->getUser();
      user.setOnline(false);
      json ws;
      ws["userID"] = std::to_string(user.getId());
      server_ws_broadcast(Event::user_offline, ws);
    }
  }
}

optional<json> tryJsonParse(std::string const& text) {
  try {
    return json::parse(text);
  } catch (...) {
    return nullopt;
  }
}

void connection_send(shared_ptr<WsServer::Connection> const& connection,
                     std::string const& evt,
                     optional<json> const& data = nonstd::nullopt) {
  json mega_data;
  mega_data["evt"] = evt;
  if (data.has_value()) {
    mega_data["data"] = *data;
  }
  auto send_stream = std::make_shared<WsServer::SendStream>();
  *send_stream << mega_data.dump();
  connection->send(send_stream);
}

void connection_init(shared_ptr<WsServer::Connection> const& connection) {
  connection_send(connection, Event::pingdata);
}

class PingLooper;
std::unique_ptr<PingLooper> looper_instance;
class PingLooper {
 private:
  HttpIoService service;
  boost::asio::deadline_timer timer;
  static constexpr int interval = 10;

  void start() {
    timer.expires_from_now(boost::posix_time::seconds(interval));
    start_wait();
  }

  void start_wait() {
    timer.async_wait(boost::bind(&PingLooper::execute, this,
                                 boost::asio::placeholders::error));
  }

  void execute(boost::system::error_code const& e) {
    if (e != boost::asio::error::operation_aborted) {
      auto conns = ws_server.get_connections();
      for (auto const& conn : conns) {
        connection_send(conn, Event::pingdata);
      }

      timer.expires_at(timer.expires_at() +
                       boost::posix_time::seconds(interval));

      start_wait();
    }
  }

 public:
  PingLooper(HttpIoService service) : service(service), timer(*service) {
    service->post(boost::bind(&PingLooper::start, this));
  }
  static void startLoop(HttpIoService service) {
    looper_instance = std::make_unique<PingLooper>(service);
  }
};

void server_ws_broadcast(std::string evt, nlohmann::json& json) {
  get_io_service()->post([evt, json]() {
    auto conns = ws_server.get_connections();
    for (auto const& conn : conns) {
      connection_send(conn, evt, json);
    }
  });
}

void server_ws_send(std::string evt, nlohmann::json& json, uuid session) {
  get_io_service()->post([evt, json, session]() {
    auto it = authedConnections.equal_range(session);
    for (auto it2 = it.first; it2 != it.second; ++it2) {
      auto conn = it2->second;
      connection_send(conn, evt, json);
    }
  });
}

void server_ws_init() {
  ws_server.config.timeout_idle = 300;
  auto& ep = ws_server.endpoint[".*"];
  ep.on_open = [](shared_ptr<WsServer::Connection> connection) {
    printf("New connection: %s\n",
           connection->remote_endpoint_address().c_str());

    connection_init(connection);
  };
  ep.on_close = [](shared_ptr<WsServer::Connection> connection, int status,
                   std::string const& reason) {
    printf("Closed connection: %s %d %s\n",
           connection->remote_endpoint_address().c_str(), status,
           reason.c_str());
    removeConn(connection);
  };

  ep.on_error = [](shared_ptr<WsServer::Connection> connection,
                   const boost::system::error_code& ec) {
    printf("Errored connection: %s %s\n",
           connection->remote_endpoint_address().c_str(), ec.message().c_str());
    removeConn(connection);
  };

  ep.on_message = [](shared_ptr<WsServer::Connection> connection,
                     shared_ptr<WsServer::Message> message) {
    std::string msgStr = message->string();
    json msg;
    std::string evt;
    try {
      msg = json::parse(msgStr);
      evt = msg["evt"];
    } catch (...) {
      return;
    }
    // Check for session info
    if (evt == Event::pongdata) {
      try {
        auto it = msg.find("data");
        if (it == msg.end() || it->type() != json::value_t::object) {
          return;
        }
        auto it2 = it->find("sessionID");
        if (it2 == it->end() || it2->type() != json::value_t::string) {
          return;
        }
        std::string sessionId = *it2;
        if (sessionId.size() != 32) {
          return;
        }
        auto u = string_to_uuid(sessionId);
        if (!u.has_value()) {
          return;
        }

        // Add to maps
        WsServer::Connection* raw = connection.get();
        if (reverseAuthedConnections.find(raw) ==
            reverseAuthedConnections.end()) {
          // Check that it's valid
          auto sessObj = Session::getSession(*u);
          if (!sessObj.has_value()) {
            return;
          }

          printf("Client %s gave session %s\n",
                 connection->remote_endpoint_address().c_str(),
                 sessionId.c_str());

          authedConnections.emplace(*u, connection);
          reverseAuthedConnections.emplace(raw, *u);

          // Emit user/online
          User user = sessObj->getUser();
          user.setOnline(true);
          json ws;
          ws["userID"] = std::to_string(user.getId());
          server_ws_broadcast(Event::user_online, ws);
        }
      } catch (...) {
        return;
      }
    }
  };

  running = true;
  PingLooper::startLoop(get_io_service());
}

void server_ws_kill() {
  running = false;
  ws_server.stop();
}

void server_ws_upgrade(const std::shared_ptr<WsServer::Connection>& conn) {
  ws_server.upgrade(conn);
}
}  // namespace dc